iT邦幫忙

2022 iThome 鐵人賽

DAY 25
1
Modern Web

一次打破 React 常見的學習門檻與觀念誤解系列 第 25

[Day 25] Reusable state — React 18 的 useEffect 在 mount 時為何會執行兩次?

  • 分享至 

  • xImage
  •  

在前面的章節中我們已經詳細的解析了 useEffect 正確的概念以及用法,也再三強調了 useEffect 的用途是同步資料到 React elements 以外的 effect 效果,且 dependencies 是一種「忽略某些不必要的執行」的效能最佳化

useEffect 的 dependencies 作為一種效能最佳化,它的檢查與略過 effect 的行為不是邏輯保證的,effect 有可能會在你意想不到的時候重新再次被執行。 因此,對 dependencies 誠實不只是一種推薦的 best practice,而是如果你嘗試逆流而行,在未來版本的 React 中你的 effect 很有可能真的會壞掉,導致應用程式出現非預期的行為。接下來我們就從 React 18 中的 useEffect 行為改動來深入這個議題。


React 18 的 useEffect 在 mount 時會執行兩次?

你可能會覺得如果我們將 dependencies 填上 [] 的話,effect 在生命週期中就永遠不會再次被執行:

function App() {
  const [count, setCount] = useState(0);
 
  useEffect(
    // 在 React 18 的 development env + strict mode 時,
    // 這個 effect 在 mount 時會執行兩次
    () => {
			console.log('effect start'); 
      setCount((prevCount) => prevCount + 1);
      
      return () => {
        console.log('effect cleanup');
      }
    },
    []
  );
 
  return <div>{count}</div>;
}

而實際上,這個範例中的 effect 在 React 18 中有可能會在 mount 時被執行兩次,你可以自己到這個 CodeSanbox 試試看。這是一個 React 18 開始才有的 breaking change,不過它只會發生在啟用 Strict mode 且為 dev build 的 React 版本中。當沒有使用 Strict mode,或是 React 為 production build 時,則不會有這個行為的改動:

import { StrictMode } from 'react';
import ReactDOM from "react-dom/client";
import App from './App';

const root = ReactDOM.createRoot(document.getElementById("root-container"));
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

你可能會對於 React 18 的這個改動感到有些詫異,開發時跟正式上線時的 effect 執行次數不一樣不是很奇怪嗎?其實,這個改動的主要目的其實是為了幫助開發者「檢查不夠安全可靠的 effect 行為」。而要解釋為何需要這種相對嚴格的檢查,我們得介紹 React 在未來版本規劃中一個全新的概念:Reusable State


Reusable State

React 在未來版本的規劃中,有許多新功能們都有一個對於開發者程式碼的共同要求,就是「你的 component 必須要設計得有足夠的彈性來多次 mount & unmount 也不會壞掉」。在大多數的情況下,我們的 component 在畫面定義的部分都是相當宣告式的所以沒問題,然而如果 component 中的 effect 在多次重複執行的情況下就會壞掉的話,則無法滿足這個要求。

其實已經有一個我們很常使用的功能依賴了這個要求與特性,就是 Fast Refresh,或是有些人習慣用它前身的 Hot module replacement 稱呼它。這個功能很常見於 React 的開發環境當中,像是 Create react app 或是 Next.js 都有內置這個工具。效果其實就是當你在開發編輯你的 component 程式碼時,每當你一存檔,你的瀏覽器就可以在不重新整理頁面的情況下即時套用 React component 的變動,而這個動作其實就會在你每次存檔時嘗試 unmount 你的 component,然後再次立即以新版的 component 程式碼去重新 mount 它,並且這個過程會保留 component 的 state 令其不會因為 unmount 被清除。

在 React 未來的計劃中,有許多的新功能與特性都會需要你的 React 程式碼滿足這個要求與限制才能正常的運作。例如一個名為 Offscreen API 的新功能,讓 React 可以在 UI 切換時保留 component 的 state 以及對應的真實 DOM elements,像是把他們暫時隱藏起來而不是真的移除它們。當這個 component 有再次顯示的需要時,就能以之前留下來的 state 狀態再次 mount,以利於我們在頻繁的畫面切換時可以提升畫面顯示處理的效能

這種能夠保留 component 的 state 狀態以便需要時快速再次還原並 mount 的概念,就是 reusable state。這種功能所連帶導致的行爲代表在 React 未來的版本中,component 有可能會「在生命週期裡 mount & unmount 不止一次而已」。

然而,有鑒於前面篇章中提及過的,在 hooks 推出以來至今還是有非常多開發者仍以不安全的方式在設計 effect,而這些 component 就可能在重複 mount 的情況下會壞掉。所以為了確保你的 component 能支援上述的特性,effect 就必須要設計的有足夠的彈性,得是無論重複執行幾次也不會壞掉

因此,為了替未來這些嚴格要求 effect 彈性的新功能及早做準備,React 才在 18 版的 Strict mode 中添加了這個模擬「mount ⇒ unmount ⇒ mount」流程的行為,所以你才會看到 component 在 mount 時連續出現「執行 effect ⇒ 執行 cleanup ⇒ 執行 effect」的動作。這其實就是以模擬多次 mount 的動作,來幫助開發者檢查 component 中的 effect 設計是否滿足這個彈性的要求。

當然,如果你的 React 專案中仍有一些彈性設計不足的 effects,你或許可以考慮在升上 React 18 之後暫時不要啟用 Strict mode 以避免開發上的困擾。不過為了讓你的 React 專案在未來也能夠享受這些全新架構與功能所帶來的好處,我們應該從今天起有意識的提醒自己要開始用更安全可靠的方式來設計 effects 與 cleanups,並視情況漸漸的調整專案中 effects 的彈性,來為未來及早做準備。

這種思維的轉換確實不是一件容易的事情,不過一旦你能夠將這些思維與觀念漸漸內化到你的思考模型中,就能夠開始在實戰中創造出直覺且品質可靠的程式碼。

在下一個篇章中,也是關於 useEffect 主題的最後一個篇章,我們將會來介紹並分享一些常見情境的 effects & cleanups 設計技巧,幫助大家在實戰中能夠更能得心應手的應對,設計出符合彈性與可靠性要求的 component effects。


參考資料


2024/2 更新 - 實體書平裝版本預購

在經過快要一年的努力後,本系列文的實體書版本推出了~其中新增並補充了許多鐵人賽版本中沒有的脈絡與細節,並以全彩印刷拉滿視覺上的閱讀體驗,現正熱銷中!有興趣的話歡迎參考看看,也歡迎分享給其他有接觸前端的朋友們,非常感謝大家~

《React 思維進化:一次打破常見的觀念誤解,躍升專業前端開發者》

目前首刷的軟精裝版本各大通路已經幾乎都銷售一空,接下來會再刷推出新的平裝版本:

天瓏(平裝版預購):
https://www.tenlong.com.tw/products/9786263337695

博客來(平裝版):
https://www.books.com.tw/products/0010982322

momo(平裝版):
https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=12528845


上一篇
[Day 24] useEffect dependencies 的經典錯誤用法
下一篇
[Day 26] Effects & cleanups 常見情境的設計技巧
系列文
一次打破 React 常見的學習門檻與觀念誤解30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言